package com.ilikesoup.instant.monitor.sample;

import com.ilikesoup.instant.monitor.core.*;
import net.sf.cglib.beans.BeanCopier;
import net.sf.cglib.proxy.Enhancer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class BeanMonitorBuilder {
    private static final Logger logger = LoggerFactory.getLogger(BeanMonitorBuilder.class);

    private List<PropertyChangeListener> listeners;
    private Map<Predicate<Method>, ChangeEventTrigger> methodTriggerMap;
    private Function serializer;
    private boolean jdkProxy;

    public BeanMonitorBuilder() {
        listeners = new ArrayList<>();
        methodTriggerMap = new LinkedHashMap<>();
    }

    public BeanMonitorBuilder addListener(PropertyChangeListener listener) {
        listeners.add(listener);
        return this;
    }

    public BeanMonitorBuilder addMethodTrigger(Predicate<Method> methodFilter, ChangeEventTrigger trigger) {
        methodTriggerMap.put(methodFilter, trigger);
        return this;
    }

    public BeanMonitorBuilder setSerializer(Function serializer) {
        this.serializer = serializer;
        return this;
    }

    public BeanMonitorBuilder useJdkProxy() {
        this.jdkProxy = true;
        return this;
    }

    public <T> T monitor(T obj) {
        if(jdkProxy) {
            return createJdkProxy(obj);
        }
        return monitor(obj, (proxy, target) -> {
            BeanCopier beanCopier = BeanCopier.create(target.getClass(), proxy.getClass(), false);
            beanCopier.copy(target, proxy, null);
        });
    }

    public <T> T monitor(T obj, BiConsumer<T, T> propCopier) {
        try {
            if(cannotProxy(obj.getClass())) {
                return obj;
            }
            T bean = (T) createProxy(obj.getClass());
            propCopier.accept(bean, obj);
            return bean;
        } catch (Throwable e) {
            logger.error("BeanMonitor's creation failed!", e);
            return obj;
        }
    }

    public <T> T newMonitoredInstance(Class<T> clazz) {
        if(cannotProxy(clazz)) {
            return newInstance(clazz);
        }
        try {
            return (T) createProxy(clazz);
        } catch (Throwable e) {
            logger.error("BeanMonitor's creation failed!", e);
            return newInstance(clazz);
        }
    }

    private <T> T newInstance(Class<T> clazz) {
        try {
            return clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
            logger.error("Bean's creation failed!", ex);
            return null;
        }
    }

    private boolean cannotProxy(Class<?> clazz) {
        int modifiers = clazz.getModifiers();
        return Modifier.isFinal(modifiers) || Modifier.isAbstract(modifiers);
    }

    private Object createProxy(Class<?> clazz) {
        CglibBeanMonitor beanMonitor = new CglibBeanMonitor();
        Enhancer e = new Enhancer();
        e.setSuperclass(clazz);
        e.setCallback(beanMonitor);
        Object bean = e.create();
        initBeanMonitor(beanMonitor, bean);
        return bean;
    }

    private void initBeanMonitor(BeanMonitor beanMonitor, Object bean) {
        PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(bean);
        listeners.forEach(propertyChangeSupport::addPropertyChangeListener);
        beanMonitor.setPropertyChangeSupport(propertyChangeSupport);
        beanMonitor.setMonitorMethodSupport(new MonitorMethodSupport(methodTriggerMap));
        SimpleMonitorContext monitorContext = new SimpleMonitorContext();
        monitorContext.setSerializer(Optional.ofNullable(serializer).orElse(DefaultStringSerializer.INSTANCE));
        beanMonitor.setMonitorContext(monitorContext);
    }

    public <T> T createJdkProxy(T obj) {
        JdkBeanMonitor beanMonitor = new JdkBeanMonitor(obj);
        Object bean = Proxy.newProxyInstance(getClass().getClassLoader(), obj.getClass().getInterfaces(), beanMonitor);
        initBeanMonitor(beanMonitor, bean);
        return (T) bean;
    }

}
